package com.develop.data.loan.manager;

import com.develop.data.loan.manager.api.LoanRepaymentPlanManager;
import com.develop.data.loan.model.LoanInstallment;
import com.develop.data.loan.model.LoanContract;
import com.develop.data.loan.option.LoanInstallmentType;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.javamoney.moneta.Money;

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.MonetaryRounding;
import javax.money.RoundingQueryBuilder;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Slf4j
public class L0001B0001LoanRepaymentPlanManager implements LoanRepaymentPlanManager {
    //纯计算逻辑，与ORM解耦
    @Getter
    private final LoanContract loanContract;
    @Getter
    private List<LocalDateTime> loanTimelinePoints;
    @Getter
    private List<Duration> loanTimelineDurations;
    @Getter
    private List<MonetaryAmount> loanInstallmentCorpusAccruals;
    @Getter
    private List<LoanInstallment> repaymentPlan;

    public L0001B0001LoanRepaymentPlanManager(LoanContract loanContract){
        this.loanContract = loanContract;
        this.calculateTimelinePointsFixedRepaymentDay();
        this.calculateTimelineDurations();
        this.calculateInstallmentCorpusAccrued();
        this.calculateRepaymentPlanLoanInstallments();
    }

    public void calculateTimelinePointsFixedRepaymentDay(){
        List<LocalDateTime> dates = calculateTimelinePointsFixedRepaymentDayWrapped();
        this.loanTimelinePoints = dates;
    }

    public List<LocalDateTime> calculateTimelinePointsFixedRepaymentDayWrapped(){

        LocalDateTime valueDate = this.loanContract.getContractValueDate();
        int floor = this.loanContract.getContractFirstTermDaysFloor();
        int terms = this.loanContract.getContractTermTail();
        int loanDayStart = valueDate.getDayOfMonth();
        int loanMonthStart = valueDate.getMonthValue();
        int loanYearStart = valueDate.getYear();
        int loanYearEnd;
        int loanMonthEnd;

        int loanRepaymentDay = this.loanContract.getContractRepaymentDay();
        LocalDateTime loanRepaymentDateStart = LocalDateTime.of(loanYearStart, loanMonthStart, loanRepaymentDay,0,0,0);
        //起息日晚于当月固定还款日
        if ( valueDate.isAfter(loanRepaymentDateStart)) loanRepaymentDateStart = loanRepaymentDateStart.plusMonths(1);
        //合并首期
        if ( valueDate.plusDays(floor).isAfter(loanRepaymentDateStart)) loanRepaymentDateStart = loanRepaymentDateStart.plusMonths(1);

        int loanRepaymentMonthStart = loanRepaymentDateStart.getMonthValue();

        //计算最后一年
        if ((loanRepaymentMonthStart + terms) % 12  == 0){
            loanYearEnd = loanYearStart + (loanRepaymentMonthStart + terms) / 12 - 1;
        }
        else {
            loanYearEnd = loanYearStart + (loanRepaymentMonthStart + terms) / 12;
        }
        //计算最后一年的最后一个月
        loanMonthEnd =  (loanRepaymentMonthStart + terms) % 12;
        loanMonthEnd = loanMonthEnd > 0 ? loanMonthEnd : 12;

        //第一年的所有月+第一年和最后一年间每年12个月+最后一年的所有月
        //第一年的所有月，注意当月固定还款日和起息日比较
        List<LocalDateTime> dates = IntStream
                .range(loanRepaymentMonthStart, Math.min(loanRepaymentMonthStart + terms, 13))
                .mapToObj(month -> LocalDateTime.of(
                        loanYearStart,
                        month,
                        confirmRepaymentDay(loanYearStart, month, loanRepaymentDay),0,0,0)
                ).collect(Collectors.toList());
        System.out.println(dates);
        //第一年和最后一年间存在完整的年，可能没有
        dates.addAll(
                IntStream
                        .range(loanYearStart+1, loanYearEnd)
                        .boxed()
                        .flatMap(year -> IntStream.range(1,13).boxed().map(month-> LocalDateTime.of(year, month, confirmRepaymentDay(year, month, loanRepaymentDay), 0 , 0, 0)))
                        .collect(Collectors.toList())
        );
        System.out.println(dates);
        //最后一年的所有月，可能没有
        if (loanYearEnd > loanYearStart){
            dates.addAll(
                    IntStream
                            .range(1, loanMonthEnd + 1)
                            .boxed()
                            .map(month-> LocalDateTime.of(loanYearEnd, month, confirmRepaymentDay(loanYearEnd, month, loanRepaymentDay), 0 , 0, 0))
                            .collect(Collectors.toList())
            );
        }
        //起息日+所有还款日
        dates.add(0, this.loanContract.getContractValueDate());
        return dates;
    }

    public void calculateTimelineDurations(){
        //每期首尾左闭右开，每期天数算头不算尾
        List<LocalDateTime> points = this.loanTimelinePoints;
        this.loanTimelineDurations = IntStream
                .range(1, points.size()).boxed()
                .map(i -> Duration.between(points.get(i-1), points.get(i)))
                .collect(Collectors.toList());
    }

    public int confirmRepaymentDay(int year, int month, int day){
        //固定还款日若为31号不是每个月都有
        YearMonth yearMonth = YearMonth.of(year, month);
        return Math.min(day, yearMonth.lengthOfMonth());
    }

    public void calculateInstallmentCorpusAccrued() {

        //计算前，"分离"金额数值和货币单位，数学公式计算，比起Money，BigDecimal更合适
        BigDecimal principal = this.loanContract.getContractEncashAmount().getNumber().numberValue(BigDecimal.class);
        String currencyCode = this.loanContract.getContractEncashAmount().getCurrency().getCurrencyCode();
        BigDecimal rateDaily = new BigDecimal(this.loanContract.getContractRateValue());
        List<LocalDateTime> points = this.loanTimelinePoints;
        List<Duration> durations = this.loanTimelineDurations;
        int termTail = this.loanContract.getContractTermTail();

        List<BigDecimal> x = durations.stream()
                .map(
                        duration -> {
                            //1 + rateDaily * duration
                            BigDecimal days = new BigDecimal(duration.toDays());
                            return days.multiply(rateDaily).add(BigDecimal.ONE);
                        }
                )
                .collect(Collectors.toList());
        Collections.reverse(x);

        BigDecimal top = principal.multiply(
                x.stream().reduce(BigDecimal.ONE, BigDecimal::multiply)
        );

        BigDecimal bottom = IntStream.range(1, x.size()).boxed()
                .map(multiSize -> x.stream().limit(multiSize).reduce(BigDecimal.ONE, BigDecimal::multiply))
                .reduce(BigDecimal.ONE, BigDecimal::add);

        BigDecimal result = top.divide(bottom, 2, RoundingMode.FLOOR);
        //计算后，"合并"金额数值和货币单位，恢复Money表示
        this.loanInstallmentCorpusAccruals = IntStream.range(0, termTail).boxed().map(i -> Money.of(result, currencyCode)).collect(Collectors.toList());
    }

    //每期应付利息，任意金额都通过函数获得，以便调整保留位数和保留方式（向上、向下、尾差）
    public MonetaryAmount calculateInstallmentInterestDueAccrual(MonetaryAmount principal, BigDecimal rateDaily, Duration duration){

        MonetaryRounding rounding = Monetary.getRounding(RoundingQueryBuilder.of().setScale(2).set(RoundingMode.FLOOR).build());

        return principal.multiply(rateDaily).multiply(new BigDecimal(duration.toDays())).with(rounding);
    }

    //每期应付本金，任意金额都通过函数获得，以便调整保留位数和保留方式（向上、向下、尾差）
    public MonetaryAmount calculateInstallmentPrincipalAccrued(MonetaryAmount corpus, MonetaryAmount interest){
        //每期总额已设置精度
        //每期利息已设置精度
        return corpus.subtract(interest);
    }

    //期末剩余本金
    public MonetaryAmount calculateContractPrincipalAccruedClosing(MonetaryAmount openingBalance, MonetaryAmount amountIncurred){
        return openingBalance.subtract(amountIncurred);
    }
    public LoanInstallment calculateRepaymentPlanLoanInstallment(int term, LoanInstallment lastLoanInstallment) {

        int termTail = this.loanContract.getContractTermTail();
        MonetaryAmount principal = this.loanContract.getContractEncashAmount();
        BigDecimal rateDaily = new BigDecimal(this.loanContract.getContractRateValue());
        List<LocalDateTime> points = this.loanTimelinePoints;
        List<Duration> durations = this.loanTimelineDurations;
        MonetaryAmount installmentCorpusDueAccrual = this.loanInstallmentCorpusAccruals.get(term-1);

        LocalDateTime installmentHead = loanTimelinePoints.get(term - 1);
        LocalDateTime installmentTail = loanTimelinePoints.get(term);

        //声明变量
        //期初应还本金
        MonetaryAmount contractPrincipalAccruedOpening = null;
        //本期应还利息
        MonetaryAmount installmentInterestDueAccrual = null;
        //本期应还本金
        MonetaryAmount installmentPrincipalAccrued = null;
        //期末应还本金
        MonetaryAmount contractPrincipalAccruedClosing = null;

        //首期，递归转折点
        if (term == 1){
            //期初应还本金
            contractPrincipalAccruedOpening = principal;
            //本期应还利息
            installmentInterestDueAccrual = this.calculateInstallmentInterestDueAccrual(contractPrincipalAccruedOpening, rateDaily, durations.get(term-1));
            //本期应还本金
            installmentPrincipalAccrued = this.calculateInstallmentPrincipalAccrued(installmentCorpusDueAccrual, installmentInterestDueAccrual);
            //期末应还本金
            contractPrincipalAccruedClosing = this.calculateContractPrincipalAccruedClosing(contractPrincipalAccruedOpening, installmentPrincipalAccrued);
        }

        //在计算等额本息每期应还金额时，结果向下取整，在分期的最后一期规避尾差
        if (term == termTail){
            //期初应还本金
            contractPrincipalAccruedOpening = lastLoanInstallment.getContractPrincipalAccrualClosing();
            //本期应还利息
            installmentInterestDueAccrual = this.calculateInstallmentInterestDueAccrual(contractPrincipalAccruedOpening, rateDaily, durations.get(term-1));
            //本期应还本金
            installmentPrincipalAccrued = contractPrincipalAccruedOpening;
            //期末应还本金
            contractPrincipalAccruedClosing = Money.of(BigDecimal.ZERO, contractPrincipalAccruedOpening.getCurrency());
        }

        //既非首期，也非尾期
        if (term != 1 && term != termTail) {
            //期初应还本金
            contractPrincipalAccruedOpening = lastLoanInstallment.getContractPrincipalAccrualClosing();
            //本期应还利息
            installmentInterestDueAccrual = this.calculateInstallmentInterestDueAccrual(contractPrincipalAccruedOpening, rateDaily, durations.get(term-1));
            //本期应还本金
            installmentPrincipalAccrued = this.calculateInstallmentPrincipalAccrued(installmentCorpusDueAccrual, installmentInterestDueAccrual);
            //期末应还本金
            contractPrincipalAccruedClosing = this.calculateContractPrincipalAccruedClosing(contractPrincipalAccruedOpening, installmentPrincipalAccrued);
        }

        return LoanInstallment.builder().installmentTerm(term)
                .installmentCorpusDueAccrual(installmentCorpusDueAccrual)
                .installmentPrincipalDueAccrual(installmentPrincipalAccrued)
                //.installmentPrincipalCleared(Money.zero(Monetary.getCurrency("CNY")))
                //.installmentPrincipalOverdue(Money.zero(Monetary.getCurrency("CNY")))
                .installmentInterestDueAccrual(installmentInterestDueAccrual)
                //.installmentInterestAccrual(Money.zero(Monetary.getCurrency("CNY")))
                //.installmentInterestCleared(Money.zero(Monetary.getCurrency("CNY")))
                //.installmentInterestOverdue(Money.zero(Monetary.getCurrency("CNY")))
                //.installmentInterestPenalty(Money.zero(Monetary.getCurrency("CNY")))
                .contractPrincipalAccrualOpening(contractPrincipalAccruedOpening)
                .contractPrincipalAccrualClosing(contractPrincipalAccruedClosing)
                .loanContract(loanContract)
                .installmentType(LoanInstallmentType.FACT_NOW)
                .installmentHead(installmentHead)
                .installmentTail(installmentTail)
                .build();
    }

    private void calculateRepaymentPlanLoanInstallments(){
        //递归改迭代的方式之一，递归是自己调用自己，把调用自己改成传入调用自己输出的结果
        int termTail = this.loanContract.getContractTermTail();
        List<LoanInstallment> result = new ArrayList<>(termTail);
        IntStream.range(0, termTail).forEach(i -> {
            LoanInstallment loanInstallment = calculateRepaymentPlanLoanInstallment(i+1, i>0?result.get(i-1):null);
            result.add(loanInstallment);
        });
        this.repaymentPlan = result;
    }


}
